昨日我們聊了一些關於「頁面」與「元件」在規劃上,可能需要注意的地方。今天,我們會實際帶著「頁面還是元件」這樣的問題意識,來實作新增英雄表單功能,在表單方面,我們將演示範本驅動的表單。
Angular 在表單方面,提供了「範本驅動」(Template driven form)及「響應式」(Reactive From)兩種不同的方式。簡要來說,前者多數程式會撰寫在 HTML 檔案、處理較簡單的表單內容,後者多數程式會撰寫在 TS 檔案,可以更好地處理邏輯較複雜的表單。
(你終於領悟了要怎麼召喚英雄了。不過,你後面揹一個大卷軸是在幹嘛?)
看來妳是不懂什麼叫做仙人模式啊,大閒人...話說妳消失了很多天?
(哼哼...上次因為經費不足讓英雄陣容縮編,讓我痛定思痛...)
妳真的去買狗狗幣了?
(不,我發現應該要尋找廉價勞工熱血的有志之士!)
聽著都不合法啊。
(你知道有一種人,不怎麼厲害,但是怎麼打也打不死。然後突然就小宇宙爆發了嗎?)
我好像知道妳在說什麼...
(而且就紀錄片來看,他們根本是弒神專門戶啊!)
呃,妳說的紀錄片是平面的還是 3D?
圖片來源:GreatGame
(特別是有個叫瞬的,只要招募到他,連他哥哥都會免費加入了,買一送一!而且他哥超厲害。)
妳再說下去,我都不知道自己在睡覺還是中了什麼幻魔拳了。
(總之我們快想辦法來讓他們簽下去吧!)
參考資料:不負責任瘋動漫。《【特別加映】聖鬥士星矢之五小強成長史「黃金12宮篇」》。
如同昨日的討論,「新增英雄」功能會需要填寫「英雄資料表單」,而可以想見的是,日後大概率會提供「編輯英雄」功能,而它也應該是編輯一樣的「英雄資料表單」。但如果我們直接將「英雄資料表單」元件當作兩個功能路徑對應的頁面的話,那可能會需要額外處理邏輯,幅度隨兩個頁面在畫面上的異同程度增減。
一個比較好的做法應該是,建立兩個頁面層級的元件「新增英雄」頁面元件及「編輯英雄」頁面元件,並將「英雄資料表單」作為一個共用元件,目前的專案結構規劃應該是這樣:
src
⌞app
⌞ pages
⌞ AddHeroPageComponent
⌞ EditHeroPageComponent
⌞ shared
⌞ components
HeroInfomationFormComponent
依序輸入下列指令:
ng g c pages/add-hero-page --skip-selector // 新增英雄頁面元件
ng g c pages/edit-hero-page --skip-selector // 編輯英雄頁面元件
ng g c shared/components/hero-information-form // 英雄資訊表單元件
並配置對應的路由,編輯 app-routing.module.ts
,將兩個頁面元件配置在路徑 'heroes' 下的 'add'、'edit':
const routes: Routes = [
{
path: '',
redirectTo: '/heroes',
pathMatch: 'full'
},
{
path: 'heroes',
children: [
{
path: '',
component: HeroListComponent
},
{
path: ':id',
component: HeroDetailComponent,
},
{
path: 'add',
component: AddHeroPageComponent
},
{
path: 'edit',
component: EditHeroPageComponent
}
]
},
]
這時候如果我們啟動應用程式,並輸入路徑 http://localhost:4200/heroes/add
或 http://localhost:4200/heroes/edit
會發現畫面一片空白,並且 console 都會出現 response error:
發生了什麼事呢?這是因為我們將新增的兩個路由配置到參數路由 :id
之後,因此,接在 heroes
之後的 add
和 edit
均被視為 id 參數,因此導向 HeroDetailComponent 並執行取得個別英雄資料的方法 getHero(heroId),因為後台查詢不到匹配這兩個 id( add
、edit
) 的英雄,因此產生 Response Error。
因為一旦路由匹配成功,就不會繼續往下觀察路徑。所以放在參數路徑之後的路由都是無效的,我們應將新增的兩個路由配置到參數路由之前:
const routes: Routes = [
{
path: '',
redirectTo: '/heroes',
pathMatch: 'full'
},
{
path: 'heroes',
children: [
{
path: '',
component: HeroListComponent
},
{
path: 'add',
component: AddHeroPageComponent
},
{
path: 'edit',
component: EditHeroPageComponent
},
{
path: ':id',
component: HeroDetailComponent,
},
]
},
]
如此一來,就能正常進入新配置的路由:
接著讓我們來實作英雄資訊表單這個共用元件。
目前我們的英雄資料模型如下(hero.model.ts):
export interface Hero {
id?: number; // id
name: string; // 姓名
image?: string; // 圖像
hp: string; // 生命值
attack: number; // 攻擊力
defence: number; // 防禦力
weapon?: string; // 武器
skill?: string; // 必殺技
description: string; // 人物介紹
}
這邊作了稍微的調動,我們將 id 屬性給為選擇性的(可以不提供這個屬性)。為什麼呢?這是因為大多時候,id 是由後端產生的,也就是說,在新增英雄時我們不需要傳送 id 屬性。
當然這可能不是一個很好的做法,也許會造成解讀上的誤會(原來英雄可以不用有 id?)。可以採用的方法至少有:
不過為了方便演示,目前我們先將 id 屬性作為一個選填屬性,將焦點放在完成表單。
首先,我們要在 AppModule 先匯入 FormsModule,這樣我們才可以使用 Template-driven Form 相關的指令:
import { FormsModule } from '@angular/forms';
(略)
@NgModule({
(略)
imports: [
(略)
FormsModule,
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
我們以 name 屬性為例來討論,在範本驅動表單的模式下,可以寫為下面這樣(hero-information-form.component.html
):
<div class="form-field">
<label for="name">NAME</label>
<input
#tName="ngModel"
name="name"
ngModel
required
type="text"
id="name" />
</div>
解釋一下與 Angular 相關的程式碼。
最核心的就是 ngModel
指令,這個指令會產生一個表單控制項(FormControl)的實例。聽起來就控場控很大,沒錯!表單控制項就是 Angular Form 的場控基石(連歐拉夫都解不了)。表單控制項可以讓我們獲得該欄位的狀態資訊,比較常使用到的包含:
並且會提供相應於上述表單控制狀態的 class(例如不合法時提供 ng-invalid class),因此,你可以很方便地完成表單狀態樣式的顯示。
當你使用了 ngModel 指令後,你就可以繼續使用檢核相關的指令,例如:
在姓名欄位,我們使用了 required 檢核指令,標示這是一個必填欄位。
這樣我們就完成了一個表單欄位的設定。但在畫面上,我們常常需要知道檢核狀態,例如需要知道它是否有錯誤、要顯示錯誤訊息。因此我們把這個表單控制項的實例指派給一個範本參考變數(也就是 #tName
)。如此一來,我們就可以在 HTML 檔案中,以 tName 來使用表單控制項提供的各種場控技能。
例如我們新增一個「儲存」按鈕,並設置它在這個名稱欄位不合法的時候(沒有填寫)是 disabeld 的:
<div class="form-field">
<label for="name">NAME</label>
<input
#tName="ngModel"
name="name"
ngModel
required
type="text"
id="name" />
</div>
<button
type="button"
[disabled]="tName.invalid">
儲存
</button>
我們先在 AddHeroPageCompoent 使用這個表單元件來看看效果:
<h1>新增英雄</h1>
<app-hero-information-form></app-hero-information-form>
畫面如下,在沒有輸入值的時候,按鈕無法點擊的:
當輸入之後,就可以點擊了:
透過範本參考變數(Template variables)#tName 和表單控制項實例(ngModel)的配合,就可以很輕鬆地產出動態檢核欄位。明天會完成這個英雄資訊表單,並優化它的畫面。
今天的程式碼已推上 Github。